IPF Provider
Provider supplying a list of data about trading instruments that can be set on the chart, along with their trading hours.
/*** Interface for receiving instrument profile data - list of [InstrumentData]** When loading dxCharts library, a list of instruments, their descriptions, etc. are taken from this interface** When connecting dxCharts library, developer can implement this interface or use the default implementation [com.devexperts.dxcharts.provider.ipf.DxFeedHttpIpfProvider] and pass it to the library using [DxChartsDataProviders] data class** Use [dataFlow] to get instrument profile data*/interface DxChartsIPFProvider {/*** Flow of receiving instrument profile data** Instrument profile data is represented by list of [InstrumentData]*/val dataFlow: StateFlow<List<InstrumentData>>val isLoading: StateFlow<Boolean>}
Data is sent by updating the state of the dataFlow
variable. It is represented as a list of com.devexperts.dxcharts.provider.domain.InstrumentData
with objects, which stores all data about the selected instrument:
/*** Data class for storing data about instruments** @param type Type of instrument (f.e. STOCK, CRYPTO)* @param symbol Symbol of instrument (f.e. GOOG, TSLA, AAPL)* @param description Description of instrument (f.e. Alphabet Inc. - Class C Capital Stock)* @param country Short name of a country of the instrument (f.e. US)* @param currency Short name of a currency of the instrument (f.e. USD)* @param priceIncrements Minimal price increments of the instrument (f.e. 0.01)* @param tradingHours Trading hours of the instrument. (e.g., "BIST(name=BIST;tz=Asia/Istanbul;hd=TR;sd=TR;td=12345;de=+0000;0=10001800)")*/data class InstrumentData(val type: String,val symbol: String,val description: String,val country: String,val currency: String,val priceIncrements: String,val tradingHours: String,)
Received data about instruments is displayed in the list of instruments:
Here is the default implementation of DxChartsIPFProvider
:
/*** Default implementation of [DxChartsIPFProvider] that uses HTTP requests to retrieve instrument data* and trading hours information from the DxFeed API.** This class provides a data flow of [InstrumentData] through a [StateFlow] and periodically* updates the data by making HTTP requests to the DxFeed API. It also retrieves and merges* trading hours information for the instruments.** @property gson Gson instance for JSON parsing.* @property _dataFlow Internal [MutableStateFlow] for emitting and collecting the instrument data.* @property dataFlow Public [StateFlow] that exposes the instrument data to external observers.* @property client [OkHttpClient] for making HTTP requests.* @property request HTTP request configuration for retrieving instrument data.* @property loaded Flag indicating whether data has been loaded successfully.* @property _errorFlow Internal [MutableStateFlow] for sending errors.* @property errorFlow [StateFlow] for sending errors.* @property job Job instance for managing the coroutine responsible for checking data.*/class DxFeedHttpIpfProvider(url: String = "",token: String = "",) : DxChartsIPFProvider, DxChartsErrorProvider<IpfProviderError> {private val gson = Gson()private val _dataFlow: MutableStateFlow<List<InstrumentData>> = MutableStateFlow(emptyList())override val dataFlow: StateFlow<List<InstrumentData>> get() = _dataFlowprivate val client = OkHttpClient()private val request = Request.Builder().url(url).addHeader("Authorization", token).build()@Volatileprivate var loaded = falseprivate val _isLoading: MutableStateFlow<Boolean> = MutableStateFlow(false)override val isLoading: StateFlow<Boolean> get() = _isLoadingprivate val _errorFlow: MutableStateFlow<IpfProviderError?> = MutableStateFlow(null)override val errorFlow: StateFlow<IpfProviderError?> get() = _errorFlowprivate var job: Job? = null/*** Initiates the process of requesting data from the DxFeed service.** This method launches a coroutine in the background using [Dispatchers.IO] that periodically fetches data* until [loaded] is true or data is received in [dataFlow].** If a previous job is still active, it is cancelled before starting a new one.* The coroutine continues fetching data in a loop, emitting the loading state via [_isLoading],* calling [request] for data, and adding a delay of [FAIL_PAUSE_MS] between attempts.** If an error occurs during the process, it is captured and emitted through [_errorFlow].*/fun connect() {_errorFlow.tryEmit(null)if (job != null) {job?.cancel()job = null}job = CoroutineScope(Dispatchers.IO).launch {try {while (!loaded && dataFlow.value.isEmpty()) {_isLoading.emit(true)request()_isLoading.emit(false)delay(FAIL_PAUSE_MS)}} catch (e: Exception) {_errorFlow.tryEmit(IpfProviderError.UnknownError("Failed to fetch data: $e.message"))}}}/*** Stops the coroutine responsible for data retrieval.** This method cancels the running [job] if it exists, terminating the ongoing data retrieval process.** If an error occurs during the cancellation process, it is captured and emitted through [_errorFlow].*/fun disconnect() {try {job?.cancel()job = null} catch (e: Exception) {_errorFlow.tryEmit(IpfProviderError.UnknownError("Failed to shutdown executor service: $e.message"))}}/*** Makes an HTTP request to the DXFeed API to fetch instrument data.* If the request is successful, it parses the data and updates the StateFlow.* If the request fails, it logs an error message.*/private fun request() {val call = client.newCall(request)try {call.execute().use { response ->if (response.isSuccessful) {val body = response.body?.string() ?: returnval instruments = body.parseData()_dataFlow.tryEmit(instruments)loaded = true} else {_errorFlow.tryEmit(IpfProviderError.HttpError("IPF request failed with status code $response.code: $response.message"))}}} catch (e: IOException) {_errorFlow.tryEmit(IpfProviderError.NetworkError("IPF request failed - Network Error: $e.message"))}}/*** Parses the raw data string and converts it into a list of [InstrumentData] objects.** @return List of [InstrumentData] parsed from the raw data string.*/private fun String.parseData(): List<InstrumentData> {val result = mutableListOf<InstrumentData>()val typeMappings =mutableMapOf<String, Map<String, Int>>()try {this.lines().forEach { line ->if (line.startsWith("#")) {val parts = line.removePrefix("#").split("::=")if (parts.size == 2) {val type = parts[0].trim()val headers = parts[1].split(",")typeMappings[type] =headers.withIndex().associate { it.value.trim() to it.index }}} else if (line.isNotBlank()) {val parts = splitWithQuotes(line)val type = parts[0]val mappings = typeMappings[type]if (mappings != null) {result.add(InstrumentData(type = parts[mappings["TYPE"] ?: 0],symbol = parts[mappings["SYMBOL"] ?: 1],description = parts.getOrElse(mappings["DESCRIPTION"] ?: -1) { "" },country = parts.getOrElse(mappings["COUNTRY"] ?: -1) { "" },currency = parts.getOrElse(mappings["CURRENCY"] ?: -1) { "" },priceIncrements = parts.getOrElse(mappings["PRICE_INCREMENTS"] ?: -1) { "" },tradingHours = parts.getOrElse(mappings["TRADING_HOURS"] ?: -1) { "" },))}}}} catch (e: Exception) {_errorFlow.tryEmit(IpfProviderError.ParsingError("Parsing data failed: $e.message",e))}return result}/*** Splits a string by commas, ignoring commas inside quotes. If there are empty elements between commas,* an empty string is added to the result.** This function is used to process CSV-like strings where some elements may be enclosed in quotes* (so that commas inside the values are not treated as separators). It also correctly handles empty fields* between commas by inserting empty strings where necessary.** @param line The input string to be split. The string may contain quoted elements, and empty fields between commas.* @return A list of strings split by commas, taking into account quoted sections and empty fields between commas.** Example:* For the input string:* "STOCK,BASFY,BASF SE S/ADR by BASF SE,US,OOTC,,USD,,"* The function will return the list:* ["STOCK", "BASFY", "BASF SE S/ADR by BASF SE", "US", "OOTC", "", "USD", ""]*/private fun splitWithQuotes(line: String): List<String> {val result = mutableListOf<String>()val regex = Pattern.compile("""("([^"]*)"|([^,]*))(,|$)""")val matcher = regex.matcher(line)while (matcher.find()) {val value = matcher.group(2) ?: matcher.group(3) ?: ""result.add(value)}val finalResult = mutableListOf<String>()var currentIndex = 0for (part in result) {while (line.indexOf(",", currentIndex) > line.indexOf(part,currentIndex) + part.length) {finalResult.add("")}finalResult.add(part)currentIndex = line.indexOf(part, currentIndex) + part.length}return finalResult}/*** Companion object containing constants and configuration for the DxFeedHttpIpfProvider class.** @property TAG Logging tag for the class.* @property FAIL_PAUSE_MS Time to pause in case of a failed IPF request.*/companion object {private const val TAG = "DxFeedHttpIpfProvider"private const val FAIL_PAUSE_MS = 3000L}}/**- Sealed class representing different types of errors that can occur in the DxFeedHttpIpfProvider.*/sealed class IpfProviderError(override val message: String, override val error: Throwable?) :ProviderError {data class NetworkError(override val message: String,override val error: Throwable? = null) : IpfProviderError(message, error)data class ParsingError(override val message: String,override val error: Throwable? = null) : IpfProviderError(message, error)data class HttpError(override val message: String,override val error: Throwable? = null) : IpfProviderError(message, error)data class UnknownError(override val message: String,override val error: Throwable? = null) : IpfProviderError(message, error)}